package com.zhuiYun.project.api.seckill.interceptor;

import com.zhuiYun.project.annotation.AccessLimit;
import com.zhuiYun.project.common.errorCode.CommonException;
import com.zhuiYun.project.common.errorCode.EmErrorCode;
import com.zhuiYun.project.util.IPUtil;
import com.zhuiYun.project.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

/**
 * @author: HKY
 * @date: 2023年04月12日 8:28
 * @ClassName: AccessLimitInterceptor
 * @Description: 限流拦截器
 * 拦截器拦截有该注解的请求，没有的则放行，有的则将该请求地址加入redis，以url作为key，并设置过期时间。每有一次求情则让该key的值增大一次。
 */
@Configuration
@Slf4j
public class AccessLimitInterceptor implements HandlerInterceptor {

    //preHandle在控制器处理请求前调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果请求是跨域请求(请求方法是:OPTIONS),handler对象不是HandlerMethod
        //系统会先发送一条预请求,请求路径与真实请求一样,但是请求方式为OPTIONS它不属于handler实例,
        //如果我们不写下面这个判断,它会继续后面的操作,但是由于预请求没有携带参数,
        //在后面的判断中就会被直接拦截下来
        if (handler instanceof HandlerMethod){
            //HandlerMethod类用于封装控制器方法信息，包含类信息、注解、方法Method对象、参数等信息
            //具体的接口请求是可以根据封装的信息调用具体的方法来执行业务逻辑；
            HandlerMethod hm = (HandlerMethod) handler;
            //获取方法注解
            AccessLimit methodAnnotation = hm.getMethodAnnotation(AccessLimit.class);
            //如果方法上没有@AccessLimit注解，证明无限流操作直接放行
            if (Objects.isNull(methodAnnotation)){
                return true;
            }
            String ipKey = IPUtil.getRemotIP(request);

            //获取注解参数值
            int seconds = methodAnnotation.seconds();
            int maxCount = methodAnnotation.maxCount();
            int ipSeconds = methodAnnotation.ipSeconds();
            int ipMaxCount = methodAnnotation.ipMaxCount();
            //获取请求路径 getRequestURI() 获取去除host（域名或IP）部分的路径
            String key=request.getRequestURI();
            // 从 Redis 中获取访问次数
            Integer count = (Integer) RedisUtil.get(key);
            Integer ipCount = (Integer) RedisUtil.get(ipKey);
            // 如果两者都为空，则表示第一次访问，需要初始化
            if (Objects.isNull(count)&& Objects.isNull(ipCount)) {
                RedisUtil.set(key, 0, seconds);
                RedisUtil.set(ipKey,0, ipSeconds);
                log.info("地址{}，在{}秒内已第一次被访问次数",key,seconds);
                log.info("ip{}，在{}秒内已第一次被访问次数",ipKey,ipSeconds);
            } else if (count<maxCount && ipCount<ipMaxCount) {
                // 访问次数未超过限制，更新 Redis 中数据
                RedisUtil.incr(key,1);
                RedisUtil.expire(key,seconds);
                RedisUtil.incr(ipKey,1);
                RedisUtil.expire(ipKey,ipSeconds);
                log.info("地址{}，在{}秒内第{}次被访问次数",key,seconds,count+1);
                log.info("IP{}，在{}秒内第{}次被访问次数",ipKey,ipSeconds,ipCount+1);
            } else {
                // 如果访问次数超过限制，则抛出异常
                log.info("IP{},在{}秒内已达到最大访问次数或地址{},在{}秒内已达到最大访问次数",ipKey,ipSeconds,key,seconds);
                throw new CommonException(EmErrorCode.SECKILL_EXPIRE);
            }
        }
        return true;
    }
}
